package com.fengwk.support.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;

import java.util.Map.Entry;

import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.ParseException;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;

import com.fengwk.support.constant.SupportConstant;
import com.fengwk.support.exception.BaseException;
import com.fengwk.support.function.Return;
import com.fengwk.support.util.LoggerUtils.Logger;

public class HttpClientUtils {
	
	/**
	 * Cookie Header
	 */
	public static final String HEADER_COOKIE = "Set-Cookie";
	
	/**
	 * 日志
	 */
	private static final Logger LOG = LoggerUtils.getLogger(HttpClientUtils.class);
	
	/**
	 * http协议
	 */
	private static final String PROTOCOL_HTTP = "http";
	
	/**
	 * https协议
	 */
	private static final String PROTOCOL_HTTPS = "https";
	
	/**
	 * query split
	 */
	private static final String QUERY_SPLIT = "?";
	
	/** 
     * 最大连接数
     */  
    private static final int MAX_CONNECTION_NUM = 150;
  
    /** 
     * 单路由最大连接数
     */  
    private static final int MAX_PER_ROUTE = 30;
	
	/**
	 * 默认从ConnectManager(连接池)获取Connection超时时间/毫秒
	 */
	private static final int DEFAULT_TIMEOUT_CONNECTION_REQUEST = 30 * 1000;
	
	/**
	 * 默认连接超时/毫秒
	 */
	private static final int DEFAULT_TIMEOUT_CONNECT = 60 * 1000;
	
	/**
	 * 默认传输(响应)超时/毫秒
	 */
	private static final int DEFAULT_TIMEOUT_SOCKET = 60 * 60 * 1000;

	/**
	 * 最大重试次数
	 */
	private static final int MAX_RETRY_COUNT = 3;
	
	/**
	 * 空白String
	 */
	private static final String JOIN_PARAMS_BLANK = "";
	
	/**
	 * 等号
	 */
	private static final String JOIN_PARAMS_EQUAL = "=";
	
	/**
	 * &
	 */
	private static final String JOIN_PARAMS_AND = "&";
	
	/**
	 * HttpClient
	 */
	private static final CloseableHttpClient HTTP_CLIENT = httpClient();

	private HttpClientUtils() {
		throw new BaseException("can not instance " + getClass());
	}
	
	/**
	 * 发送get请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity get(String url, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		return sendRequest(
				url,
				params,
				headers,
				connectTimeout,
				socketTimeout,
				() -> httpGet(url, params, connectTimeout, socketTimeout, proxyHost, proxyPort));
	}
	
	/**
	 * 发送get请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @return
	 */
	public static ResponseEntity get(String url, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout) {
		return get(url, params, headers, connectTimeout, socketTimeout, null, null);
	}
	
	/**
	 * 发送get请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity get(String url, Map<String, String> params, Map<String, String> headers, String proxyHost, Integer proxyPort) {
		return get(url, params, headers, null, null, proxyHost, proxyPort);
	}
	
	/**
	 * 发送get请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @return
	 */
	public static ResponseEntity get(String url, Map<String, String> params, Map<String, String> headers) {
		return get(url, params, headers, null, null, null, null);
	}
	
	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity post(String url, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		return sendRequest(
				url,
				params,
				headers,
				connectTimeout,
				socketTimeout,
				() -> httpPost(url, params, connectTimeout, socketTimeout, proxyHost, proxyPort));
	}
	
	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @return
	 */
	public static ResponseEntity post(String url, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout) {
		return post(url, params, headers, connectTimeout, socketTimeout, null, null);
	}
	
	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity post(String url, Map<String, String> params, Map<String, String> headers, String proxyHost, Integer proxyPort) {
		return post(url, params, headers, null, null, proxyHost, proxyPort);
	}
	
	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param params
	 * @param headers
	 * @return
	 */
	public static ResponseEntity post(String url, Map<String, String> params, Map<String, String> headers) {
		return post(url, params, headers, null, null, null, null);
	}
	
	/**
	 * 发送post请求
	 * 
	 * @param url
	 * @param params
	 * @return
	 */
	public static ResponseEntity post(String url, String[]...params) {
		Map<String, String> paramsMap = null;
		if (ArrayUtils.isNotBlank(params)) {
			paramsMap = new HashMap<>();
			for (String[] param: params) {
				paramsMap.put(param[0], param[1]);
			}
		}
		return post(url, paramsMap, null);
	}
	
	/**
	 * 上传
	 * 
	 * @param url
	 * @param fileMap
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity upload(String url, Map<String, byte[]> fileMap, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		return sendRequest(
				url,
				params,
				headers,
				connectTimeout,
				socketTimeout,
				() -> uploadPost(url, fileMap, params, connectTimeout, socketTimeout, proxyHost, proxyPort));
	}
	
	/**
	 * 上传
	 * 
	 * @param url
	 * @param fileMap
	 * @param params
	 * @param headers
	 * @param connectTimeout
	 * @param socketTimeout
	 * @return
	 */
	public static ResponseEntity upload(String url, Map<String, byte[]> fileMap, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout) {
		return upload(url, fileMap, params, headers, connectTimeout, socketTimeout, null, null);
	}
	
	/**
	 * 上传
	 * 
	 * @param url
	 * @param fileMap
	 * @param params
	 * @param headers
	 * @param proxyHost
	 * @param proxyPort
	 * @return
	 */
	public static ResponseEntity upload(String url, Map<String, byte[]> fileMap, Map<String, String> params, Map<String, String> headers, String proxyHost, Integer proxyPort) {
		return upload(url, fileMap, params, headers, null, null, proxyHost, proxyPort);
	}
	
	/**
	 * 上传
	 * 
	 * @param url
	 * @param fileMap
	 * @param params
	 * @param headers
	 * @return
	 */
	public static ResponseEntity upload(String url, Map<String, byte[]> fileMap, Map<String, String> params, Map<String, String> headers) {
		return upload(url, fileMap, params, headers, null, null, null, null);
	}
	
	// 初始化HttpClient连接池
	private static PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
		SSLContextBuilder sslContextBuilder = new SSLContextBuilder();  
    	try {
			sslContextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
		} catch (NoSuchAlgorithmException | KeyStoreException e) {
		    LOG.error(e);
		}
    	LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory();
    	ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
        		.<ConnectionSocketFactory> create()
                .register(PROTOCOL_HTTPS, sslsf)
                .register(PROTOCOL_HTTP, plainsf)
                .build();
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);  
        cm.setMaxTotal(MAX_CONNECTION_NUM);  
        cm.setDefaultMaxPerRoute(MAX_PER_ROUTE);  
        return cm;
    } 
	
	// 获取HttpClient
	private static CloseableHttpClient httpClient() {
		PoolingHttpClientConnectionManager cm = poolingHttpClientConnectionManager();
		return HttpClients
        		.custom()
                .setConnectionManager(cm)
                .setRetryHandler((exception, executionCount, context) -> {
                	if (executionCount > MAX_RETRY_COUNT)
                		return retryHandlerException(false, "重连次数超过" + MAX_RETRY_COUNT);
                	if (exception instanceof NoHttpResponseException)
                		return retryHandlerException(true, "服务器丢掉连接");
                	if (exception instanceof SSLHandshakeException)
                		return retryHandlerException(false, "SSL握手异常");
                	if (exception instanceof InterruptedIOException)
                		return retryHandlerException(false, "超时");
                	if (exception instanceof UnknownHostException)
                		return retryHandlerException(false, "服务器不可达");
	                if (exception instanceof ConnectTimeoutException)
	                	return retryHandlerException(false, "连接被拒绝");
	                if (exception instanceof SSLException)
	                	return retryHandlerException(false, "SSL异常");

	                HttpClientContext clientContext = HttpClientContext.adapt(context);
	                HttpRequest request = clientContext.getRequest();    
	                // 如果请求是幂等的，就再次尝试
                    if (!(request instanceof HttpEntityEnclosingRequest))
                    	return true;
                	return false;
                })
                .build();
	}
	
	private static boolean retryHandlerException(boolean b, String msg) {
		if (b) 
		    LOG.error(msg);
		return b;
	}
	
	// 获取HttpGet
	private static HttpGet httpGet(String url, Map<String, String> params, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		url += QUERY_SPLIT + joinParams(params);
		HttpGet httpGet = new HttpGet(url);
		setRequestConfig(httpGet, connectTimeout, socketTimeout, proxyHost, proxyPort);
    	return httpGet;
    }
	
	// 获取HttpPost
	private static HttpPost httpPost(String url, Map<String, String> params, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {   
        HttpPost httpost = new HttpPost(url);
        setRequestConfig(httpost, connectTimeout, socketTimeout, proxyHost, proxyPort);
        List<NameValuePair> nvpList = new ArrayList <NameValuePair>();  
        if(params != null) {
            for(String key : params.keySet()) {  
            	nvpList.add(new BasicNameValuePair(key, params.get(key)));  
            }
        }
        UrlEncodedFormEntity entity = null;
        try {
        	entity = new UrlEncodedFormEntity(nvpList, SupportConstant.DEFAULT_CHARSET);
		} catch (UnsupportedEncodingException e) {
		    LOG.error(e);
		}
        httpost.setEntity(entity);
        return httpost;
    }
	 
	// 上传post请求
	private static HttpPost uploadPost(String url, Map<String, byte[]> fileMap, Map<String, String> params, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {   
		HttpPost httpPost = new HttpPost(url);
        setRequestConfig(httpPost, connectTimeout, socketTimeout, proxyHost, proxyPort);
        MultipartEntityBuilder builder = MultipartEntityBuilder
        		.create()
        		.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
        		.setCharset(Charset.forName(SupportConstant.DEFAULT_CHARSET))
        		.setContentType(ContentType.MULTIPART_FORM_DATA);
        if(MapUtils.isNotBlank(fileMap)) {
            for(String key : fileMap.keySet()) {
            	ByteArrayBody cab = new ByteArrayBody(fileMap.get(key), key);
            	builder.addPart(key, cab);
            }
        }
        if(MapUtils.isNotBlank(params)) {
            for(String key : params.keySet()) {
            	builder.addTextBody(key, params.get(key));
            }
        }
        httpPost.setEntity(builder.build());
        return httpPost;
    }
	
	// 获取请求配置
	// connectTimeout 连接超时/毫秒
	// socketTimeout 请求获取数据的超时时间(即响应时间)/毫秒
	private static RequestConfig requestConfig(Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		if (connectTimeout == null)
			connectTimeout = DEFAULT_TIMEOUT_CONNECT;
		if (socketTimeout == null)
			socketTimeout = DEFAULT_TIMEOUT_SOCKET;
		HttpHost proxy = null;
		if (proxyHost != null && proxyPort != null) 
			proxy = new HttpHost(proxyHost, proxyPort);
		return RequestConfig
    			.custom()
    			.setConnectionRequestTimeout(DEFAULT_TIMEOUT_CONNECTION_REQUEST)
    	        .setConnectTimeout(connectTimeout)
    	        .setSocketTimeout(socketTimeout)
    	        .setProxy(proxy)
    	        .build();
    }

	// 设置请求配置
	private static void setRequestConfig(HttpRequestBase httpRequestBase, Integer connectTimeout, Integer socketTimeout, String proxyHost, Integer proxyPort) {
		httpRequestBase.setConfig(requestConfig(connectTimeout, socketTimeout, proxyHost, proxyPort));
	}
	
	// 设置请求头
    private static void setRequestHeaders(HttpUriRequest httpUriRequest, Map<String, String> headers) {
    	if(MapUtils.isNotBlank(headers)) {
    		Iterator<Entry<String, String>> it = headers.entrySet().iterator();
        	while(it.hasNext()) {
        		Entry<String, String> entry = it.next();
        		httpUriRequest.setHeader(entry.getKey(), entry.getValue());
        	}
    	}
    }
    
	// 发送请求
	private static ResponseEntity sendRequest(String url, Map<String, String> params, Map<String, String> headers, Integer connectTimeout, Integer socketTimeout, Return.Params0<HttpUriRequest> getHttpUriRequest) {
		HttpUriRequest request = getHttpUriRequest.apply();
		setRequestHeaders(request, headers);
		return sendRequest(request);
	}
    
    // 发送请求
    private static ResponseEntity sendRequest(HttpUriRequest httpUriRequest) {  
    	ResponseEntity responseEntity = null;
		try {
			CloseableHttpResponse httpResponse = HTTP_CLIENT.execute(httpUriRequest);
			responseEntity = httpResponseToResponseEntity(httpResponse);
		} catch (IOException e) {
		    LOG.error(e);
		}
        return responseEntity;
    }
    
	// 构建HttpResponse对象
    private static ResponseEntity httpResponseToResponseEntity(HttpResponse httpResponse) {
    	ResponseEntity responseEntity = null;
    	InputStream is = null;
    	try {
    		HttpEntity entity = httpResponse.getEntity();
    		is = entity.getContent();
    		StatusLine statusLine = httpResponse.getStatusLine();
    		responseEntity = new ResponseEntity(
					httpResponse.getAllHeaders(),
					IoUtils.toBytes(is), 
					ContentType.getOrDefault(entity),
					statusLine);
		} catch (UnsupportedCharsetException | UnsupportedOperationException | ParseException | IOException e) {
		    LOG.error(e);
		} finally {
			// 关闭连接池连接
			IoUtils.close(is);
		}
    	return responseEntity;
    }
    
    // 参数拼接
    static String joinParams(Map<String, String> params) {
		String param = JOIN_PARAMS_BLANK;
		if(MapUtils.isNotBlank(params)) {
			Iterator<Entry<String, String>> it = params.entrySet().iterator();
			StringBuffer sb = new StringBuffer();
			while(it.hasNext()) {
				Entry<String, String> entry = it.next();
				String value = entry.getValue();
				if(StringUtils.isBlank(value))
					continue;
				sb.append(entry.getKey() + JOIN_PARAMS_EQUAL + CodeUtils.urlEncode(value) + JOIN_PARAMS_AND);
			}
			param = sb.toString();
			if(StringUtils.isNotBlank(param)) 
				param = new String(param.toCharArray(), 0, param.length() -1); 
		}
		return param;
	}
    
    /**
     * http响应实体
     * 
     * @author fengwk
     *
     */
    public static class ResponseEntity implements Serializable {
		
		private static final long serialVersionUID = -4709393135317302673L;
		
		private static final Pattern PATTERN_CHARSET = Pattern.compile("<meta.*?charset[^=]*=(.+?)[\\s|>]");
		
		private Header[] headers;// http响应头部
		private byte[] content;// http响应内容
		private ContentType contentType;// ContentType
		private StatusLine statusLine;// StatusLine
		
		private ResponseEntity(Header[] headers, byte[] content, ContentType contentType, StatusLine statusLine) {
			this.headers = headers;
			this.content = content;
			this.contentType = contentType;
			this.statusLine = statusLine;
		}

		public Header[] getHeaders() {
			return headers;
		}

		public byte[] getContent() {
			return content;
		}

		public ContentType getContentType() {
			return contentType;
		}

		public StatusLine getStatusLine() {
			return statusLine;
		}

		public String getStringContent() {
			Charset charset = null;
			if (contentType != null) {
			    charset = contentType.getCharset();
			}
            if (charset == null) {
                charset = Consts.UTF_8;
            }
            ByteBuffer wrap = ByteBuffer.wrap(content);
            String str = charset.decode(wrap).toString();
            if ("text/html".equalsIgnoreCase(contentType.getMimeType())) {
            	Matcher m = PATTERN_CHARSET.matcher(str);
            	if (m.find()) {
            		String fixCharset = m.group(1);
            		if (StringUtils.isNotBlank(fixCharset) && !fixCharset.equalsIgnoreCase(charset.name())) {
            		    try {
            		        charset = Charset.forName(fixCharset);
            		        str = charset.decode(wrap).toString();
                        } catch (UnsupportedCharsetException e) {
                            LOG.error(e);
                        }
            		}
            	}
            }
			return str;
		}
		
		public Header[] getHeaders(String name) {
	        List<Header> headersFound = null;
	        for (int i = 0; i < this.headers.length; i++) {
	            Header header = headers[i];
	            if (header.getName().equalsIgnoreCase(name)) {
	                if (headersFound == null) {
	                    headersFound = new ArrayList<Header>();
	                }
	                headersFound.add(header);
	            }
	        }
	        return headersFound != null ? headersFound.toArray(new Header[headersFound.size()]) : null;
	    }
		
		public Header[] getCookies() {
			return getHeaders(HEADER_COOKIE);
		}
		
		public String getCookie(String cookieName) {
			String foundCookie = null;
			Header[] cookies = getCookies();
			if(StringUtils.isNotBlank(cookieName) || ArrayUtils.isNotBlank(cookies)) {
				for(Header cookie: cookies) {
					if(cookie.getValue().toLowerCase().indexOf(cookieName.toLowerCase()) != -1) {
						String cookieBuf = RegexUtils.regex(cookie.getValue(), "(?i)" + cookieName + "=\\S+;");
						int nameLen = cookieName.length();
						foundCookie = StringUtils.substring(cookieBuf, nameLen, cookieBuf.length() - nameLen - 1);
					}
				}
	    	}
			return foundCookie;
		}
		
	}

}
